package cz.mtq.mgsc.view;

import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Font;
import javax.microedition.lcdui.Graphics;

import cz.mtq.mgsc.MgscMidlet;
import cz.mtq.mgsc.Settings;
import cz.mtq.mgsc.command.AbstractCommand;
import cz.mtq.mgsc.command.Command;
import cz.mtq.mgsc.model.Player;

/**
 * 
 * <a href="http://minigolfscorecard.googlecode.com/">MiniGolf Scorecard</a>
 * 
 * TODO pokud behem pauzy u obou strankovani nechceme prijimat key pressed
 * udalosti, musime implementovat pres vlakna ci timery!
 * 
 * TODO opravit chybu renderovani pri horizontalnim strankovani "u starsich nokii"
 * 
 * TODO co nejmensi suma dokud to jde
 * TODO levy horni roh = info o vertical pagingu
 * 
 * @author Miroslav Tichý
 * 
 */
public class ScoreCardCanvas extends Canvas {

	// pause (in milliseconds) after some rendering occured
	private static final int PAUSE = 1000;
	// sum character
	private static final String CHAR_SIGMA = "\u03A3";
	private final Font font;
	// 0 = x, 1 = y, 2 = Ix (index of active cell starting from 0 to
	// User.NUMBER_OF_HOLES-1)
	private static final int ACTIVE_CELL_DATA_X_INDEX = 0;
	private static final int ACTIVE_CELL_DATA_Y_INDEX = 1;
	private static final int ACTIVE_CELL_DATA_IX_INDEX = 2;
	private static final int ACTIVE_CELL_DATA_IY_INDEX = 3;
	private final int[] activeCellData;
	// no change = Integer.MAX_VALUE
	private int lastKeyCode;
	// input cells - horizontal page index
	private static final int INPUT_CELLS_RENDERED_PAGE_INDEX = 0;
	// input cells - horizontal page size
	private static final int INPUT_CELLS_RENDERED_PAGE_SIZE = 1;
	// input cells - vertical page index
	private static final int INPUT_CELLS_RENDERED_VPAGE_INDEX = 2;
	// input cells - vertical page size
	private static final int INPUT_CELLS_RENDERED_VPAGE_SIZE = 3;
	private final int[] renderedInputCellsData;
	// index of the first line rendered; used for vertical paging
	// TODO remove firstRenderedLineIndex and implement vertical paging
	private int firstRenderedLineIndex = 0;
	// midlet
	private final MgscMidlet midlet;

	// modes
	private final AutoMoveMode autoMoveMode = new AutoMoveMode();
	private final ManualMoveMode manMoveMode;
	private final ManualMoveWithIncrementMode manMoveWithIncrementMode = new ManualMoveWithIncrementMode();
	private final EndOfTheGameMode endOfTheGameMode = new EndOfTheGameMode();
	// current mode
	private Mode mode;
	/**
	 * commands used for various paging or/and rendering logic
	 */
	private final HorizontalPaging horizontalPaging;
	private final LastPage lastPage;
	private final ToNextColumnMove toNextColumnMove;
	private final VerticalPaging verticalPaging;
	// Command executor
	private final AsyncCommand asyncCommand = new AsyncCommand();

	// menu commands for invoking manual move mode...
	private AbstractCommand manualMoveModeCmd;
	// ...and auto move mode
	private AbstractCommand autoMoveModeCmd;
	// ...and manual move with increment mode
	private AbstractCommand manualMoveWithIncrementModeCmd;
	// ...and end of the game mode
	private AbstractCommand endGameModeCmd;
	// canvas height and width
	private int canvasHeight, canvasWidth;

	public ScoreCardCanvas(final MgscMidlet midlet, int mainModeIndex) {
		super();
		setMainMode(mainModeIndex);
		manMoveMode = new ManualMoveMode(mode);
		this.setLastKeyCodeNotSet();
		this.setFullScreenMode(true);
		this.canvasHeight = super.getHeight();
		this.canvasWidth = super.getWidth();
		// active cell data
		activeCellData = new int[4];
		setActiveCellDataX(0);
		setActiveCellDataY(0);
		setActiveCellDataIx(0);
		setActiveCellDataIy(0);
		// input cells rendered data
		renderedInputCellsData = new int[4];
		this.setCommandListener(midlet);
		this.midlet = midlet;
		setTitle(Settings.getResource(Settings.TEXT_APP_NAME));
		// font
		font = Font.getFont(Font.FACE_MONOSPACE, Font.STYLE_PLAIN, Font.SIZE_LARGE);
		// init sizes
		Settings.initCellSize(font);
		// set mode specific (screen) commands
		mode.setCommands();
		// commands
		this.verticalPaging = new VerticalPaging();
		this.toNextColumnMove = new ToNextColumnMove();
		this.lastPage = new LastPage();
		this.horizontalPaging = new HorizontalPaging();
	}

	/**
	 * 
	 */
	protected void paint(Graphics g) {
		setFont(g, font);
		// fill (black) rectangle
		fillRectangle(g);
		prepareForGridRendering(g);
		// prepare rendering context = hole cells to render, ..., ...
		prepareRenderingContext();
		// handle key input
		mode.handleKeyEvent();
		// render heading (name and holes + sigma as sum)
		renderHeading(g);
		// render active cell
		mode.renderActiveCell(g, getActiveCellDataX(), getActiveCellDataY());
		// render input cells
		renderInputCells(g);
	}

	private void prepareRenderingContext() {
		// the maximum number of vertical lines we can draw
		int max = (getHolesAndSumCellsCanvasWidth() - 3 * Settings.getCHAR_WIDTH()) / getHoleCellWidth();
		// max-1 tells us the number of cells rendered = page size
		setRenderedInputCellsDataPageSize(max - 1);
	}

	private void handleHorizontalMove(int activeCellDataX, int activeCellDataIx) {
		// add holeCellWidth to x coordinate
		setActiveCellDataX(activeCellDataX);
		// set the index of active cell
		setActiveCellDataIx(activeCellDataIx);
	}

	private void handleMoveDown() {
		setActiveCellDataY(getActiveCellDataY() + getCellHeight());
		addActiveCellDataIy(1);
		setLastKeyCodeNotSet();
	}

	private void updateModel(byte value) {
		updateModel(value, false);
	}

	private void updateModel(byte value, boolean add) {
		Player u = getCurrentPlayer();
		int iX = getActiveCellDataIx();
		if (add) {
			u.addOneToHole(iX);
		} else {
			u.setHole(iX, value);
		}
	}

	private Player getCurrentPlayer() {
		return midlet.getPlayers().getPlayer(getActiveCellDataIy());
	}

	/**
	 * renders heading cells (name, holes as much as possible + 1 sum cell)
	 * 
	 * @param g
	 */
	private void renderHeading(Graphics g) {
		// starting at 0, getCellHeight() - rendering horizontal line
		g.drawLine(0, getCellHeight(), getWidth(), getCellHeight());
		// //rendering crossing...
		g.drawLine(0, 0, getNameCellWidth(), getCellHeight());
		g.drawLine(0, getCellHeight(), getNameCellWidth(), 0);
		// render vertical lines (cells) for holes and sum
		g.translate(getNameCellWidth(), 0);
		renderHeadingCells(g);
		g.translate(-getNameCellWidth(), 0);
	}

	/**
	 * Renders input cells = user results
	 * 
	 * @param g
	 */
	private void renderInputCells(Graphics g) {
		// starting at 0, getCellHeight()
		g.translate(0, getCellHeight());
		// rendering horizontal lines
		final int maxInputCells = getMaxInputCellsToRender();
		for (int i = firstRenderedLineIndex, y = getCellHeight(); i < midlet.getPlayers().size(); i++, y += getCellHeight()) {
			Player u = (Player) midlet.getPlayers().getPlayers().elementAt(i);
			writeValueToNameCell(u.getName(), g, 0, y - getCellHeight());
			g.drawLine(0, y, getWidth(), y);
			// we must render data model values as well
			g.translate(getNameCellWidth(), -getCellHeight());
			for (int j = 0, x = 0; j <= maxInputCells; j++, x += getHoleCellWidth()) {
				if (j != maxInputCells) {
					// render hole value
					int holeIndex = j + (getRenderedInputCellsDataPageIndex() * getRenderedInputCellsDataPageSize());
					byte value = u.getHole(holeIndex);
					if (value != 0) {
						if (i == getActiveCellDataIy() && holeIndex == getActiveCellDataIx()) {
							mode.renderValueOnActiveCell(g, String.valueOf(value), x, y);
						} else {
							writeValueWithMarginToHoleCell(String.valueOf(value), g, x, y);
						}
					}
				} else {
					// render sum
					int sumCellWidth = getSumCellWidth(x);
					byte sum = u.getSum();
					x = x + sumCellWidth - getStringWidth(String.valueOf(sum));
					setSumColor(g);
					writeValueToHoleCell(String.valueOf(sum), g, x, y);
					setForegroundColor(g);
					g.translate(-getNameCellWidth(), getCellHeight());
				}
			}
		}
		// moving to the right
		g.translate(getNameCellWidth(), 0);
		// rendering input cells - vertical lines
		// g.setClip(0, 0, getWidth(), midlet.getUsers().size()*getCellHeight());
		for (int i = 0, x = 0, h = (midlet.getPlayers().size() - firstRenderedLineIndex) * getCellHeight(); i <= maxInputCells; i++, x += Settings
				.getHOLE_CELL_WIDTH()) {
			g.drawLine(x, 0, x, h);
		}
		// cleaning
		g.translate(-getNameCellWidth(), -getCellHeight());
	}

	private void fillRectangle(Graphics g) {
		setBackgroundColor(g);
		int width = getWidth();
		int height = getHeight();
		g.fillRect(0, 0, width, height);
	}

	/**
	 * 
	 * @param x
	 * @param y
	 * @param height
	 * @param canvasWidth
	 * @param cellWidth
	 * @param g
	 * @return cells rendered
	 */
	private void renderHeadingCells(Graphics g) {
		String value = null;
		for (int i = 0, x = 0, h = getCellHeight(), maxInputCells = getMaxInputCellsToRender(); i <= maxInputCells; i++, x += Settings
				.getHOLE_CELL_WIDTH()) {
			if (i + 1 > maxInputCells) {
				// render sum cell
				value = CHAR_SIGMA;
			} else {
				value = String.valueOf(i + 1 + (getRenderedInputCellsDataPageIndex() * getRenderedInputCellsDataPageSize()));
			}
			g.drawLine(x, 0, x, h);
			setLabelColor(g);
			if (value == CHAR_SIGMA) {
				setSumColor(g);
				// must center the sigma char in the cell
				int sumCellWidth = getSumCellWidth(x);
				x = x + ((sumCellWidth - Settings.getCHAR_WIDTH()) / 2);
				writeValueToHoleCell(value, g, x, 0);
			} else {
				writeValueWithMarginToHoleCell(value, g, x, 0);
			}
			setForegroundColor(g);
		}
	}

	private void writeValueToHoleCell(String value, Graphics g, int x, int y) {
		g.drawString(value, x, y, Graphics.LEFT | Graphics.TOP);
	}

	private void writeValueWithMarginToHoleCell(String value, Graphics g, int x, int y) {
		writeValueToHoleCell(value, g, getXWithMargin(value, x), y);
	}

	private void writeValueToNameCell(String value, Graphics g, int x, int y) {
		setLabelColor(g);
		g.drawString(value, x, y, Graphics.LEFT | Graphics.TOP);
		setForegroundColor(g);
	}

	private int getXWithMargin(String value, int x) {
		return value != null && value.length() > 1 ? x + Settings.getTWO_DIGIT_MARGIN() : x + Settings.getONE_DIGIT_MARGIN();
	}

	/**
	 * Sets the color, stroke style,...
	 * 
	 * @param g
	 */
	private void prepareForGridRendering(Graphics g) {
		g.setStrokeStyle(Graphics.SOLID);
		// white
		setForegroundColor(g);
	}

	private void renderActiveCell(Graphics g, int x, int y) {
		g.translate(getNameCellWidth(), getCellHeight());
		int color = g.getColor();
		// white
		setForegroundColor(g);
		g.fillRect(x, y, Settings.getHOLE_CELL_WIDTH(), getCellHeight());
		g.setColor(color);
		g.translate(-getNameCellWidth(), -getCellHeight());
	}

	/**
	 * MIDP stuff
	 */
	protected void keyPressed(int keyCode) {
		mode.keyPressed(keyCode);
	}

	private void customKeyPressed(int keyCode) {
		lastKeyCode = keyCode;
		reRenderRegion();
	}

	protected void keyRepeated(int keyCode) {
		mode.keyRepeated(keyCode);
	}

	private void customKeyRepated(int keyCode) {
		lastKeyCode = keyCode;
		reRenderRegion();
	}

	private void reRenderRegion() {
		repaint();
	}

	private void setFont(Graphics g, Font f) {
		g.setFont(f);
	}

	public int getHeight() {
		return canvasHeight;
	}

	public int getWidth() {
		return canvasWidth;
	}

	/**
	 * Business logic
	 */

	private byte convertLastKeyCode() {
		byte value = Byte.MIN_VALUE;
		switch (lastKeyCode) {
		case Canvas.KEY_NUM1:
			value = 1;
			break;
		case Canvas.KEY_NUM2:
			value = 2;
			break;
		case Canvas.KEY_NUM3:
			value = 3;
			break;
		case Canvas.KEY_NUM4:
			value = 4;
			break;
		case Canvas.KEY_NUM5:
			value = 5;
			break;
		case Canvas.KEY_NUM6:
			value = 6;
			break;
		case Canvas.KEY_NUM7:
			value = 7;
			break;
		}
		return value;
	}

	public void swapMode(Mode newMode) {
		this.mode.beforeDeactivate();
		this.mode = newMode;
	}

	public void swapModeWithCommands(Mode newMode) {
		this.mode.clearCommands();
		swapMode(newMode);
		this.mode.setCommands();
	}

	/**
	 * This method computes and sets the
	 * {@link ScoreCardCanvas#firstRenderedLineIndex} to value which should be
	 * rendered as first on the page. Usually used when vertical paging (up)
	 * occurs.
	 * 
	 * TODO remove when vertical paging is implemented
	 */
	private int computeAndSetFirstRenderedLineIndex() {
		for (int i = firstRenderedLineIndex, y = getCellHeight(), substractedIndexes = 0; i >= 0; i--, y += getCellHeight(), substractedIndexes++) {
			if (y + getCellHeight() > getHeight()) {
				firstRenderedLineIndex = i;
				return substractedIndexes;
			}
		}
		return -1;
	}

	private void setActiveCellXStuffToTopLeft() {
		setActiveCellDataX(0);
		setActiveCellDataIx(getCurrentPageFirstHoleIndex());
	}

	private void addCommand(AbstractCommand cmd) {
		this.midlet.addCommand(this, cmd);
	}

	private void removeCommand(AbstractCommand cmd) {
		this.midlet.removeCommand(this, cmd);
	}

	private int getActiveCellDataX() {
		return activeCellData[ACTIVE_CELL_DATA_X_INDEX];
	}

	private AbstractCommand getManualMoveModeCmd() {
		if (manualMoveModeCmd == null) {
			manualMoveModeCmd = new cz.mtq.mgsc.command.AbstractCommand(new javax.microedition.lcdui.Command(Settings
					.getResource(Settings.TEXT_MANUAL_MOVE_MODE), javax.microedition.lcdui.Command.SCREEN, 1), ScoreCardCanvas.this.midlet) {

				protected void exec() throws Exception {
					execManualModeCmd(manMoveMode);
				}

			};
		}
		return manualMoveModeCmd;
	}

	private AbstractCommand getManualMoveWithIncrementModeCmd() {
		if (manualMoveWithIncrementModeCmd == null) {
			manualMoveWithIncrementModeCmd = new cz.mtq.mgsc.command.AbstractCommand(new javax.microedition.lcdui.Command(Settings
					.getResource(Settings.TEXT_MAIN_MODE), javax.microedition.lcdui.Command.SCREEN, 1), ScoreCardCanvas.this.midlet) {

				protected void exec() throws Exception {
					execManualModeCmd(manMoveWithIncrementMode);
				}

			};
		}
		return manualMoveWithIncrementModeCmd;
	}

	private void execManualModeCmd(Mode mode) {
		// clear commands of current mode
		swapModeWithCommands(mode);
		if (!isLastPage()) {
			int residual = getNumberOfHoles() % getRenderedInputCellsDataPageSize();
			int lastPageFirstColumnIndex = getNumberOfHoles() - (residual == 0 ? getRenderedInputCellsDataPageSize() : residual);
			if (getActiveCellDataIx() == lastPageFirstColumnIndex) {
				// user has moved in manual move mode from last page
				setActiveCellXStuffToTopLeft();
			}
		}
		// because active cell does not hide the number
		repaint();
	}

	private AbstractCommand getAutoMoveModeCmd() {
		if (autoMoveModeCmd == null) {
			autoMoveModeCmd = new cz.mtq.mgsc.command.AbstractCommand(new javax.microedition.lcdui.Command(Settings
					.getResource(Settings.TEXT_MAIN_MODE), javax.microedition.lcdui.Command.SCREEN, 1), ScoreCardCanvas.this.midlet) {

				protected void exec() throws Exception {
					swapModeWithCommands(autoMoveMode);
					autoMoveMode.renderActiveCell = true;
					// because active cell hides the number
					repaint();
				}
			};
		}
		return autoMoveModeCmd;
	}

	private AbstractCommand getEndOfGameModeCmd() {
		if (endGameModeCmd == null) {
			endGameModeCmd = new cz.mtq.mgsc.command.AbstractCommand(new javax.microedition.lcdui.Command(Settings
					.getResource(Settings.TEXT_END_OF_THE_GAME_MODE), javax.microedition.lcdui.Command.SCREEN, 1), ScoreCardCanvas.this.midlet) {

				protected void exec() throws Exception {
					// sort results
					ScoreCardCanvas.this.midlet.sortPlayers();
					swapModeWithCommands(endOfTheGameMode);
					repaint();
				}
			};
		}
		return endGameModeCmd;
	}

	private void setActiveCellDataX(int x) {
		activeCellData[ACTIVE_CELL_DATA_X_INDEX] = x;
	}

	private int getActiveCellDataIx() {
		return activeCellData[ACTIVE_CELL_DATA_IX_INDEX];
	}

	private void setActiveCellDataIx(int x) {
		activeCellData[ACTIVE_CELL_DATA_IX_INDEX] = x;
	}

	private int addToActiveCellDataIx(int x) {
		return getActiveCellDataIx() + x;
	}

	private int getActiveCellDataIy() {
		return activeCellData[ACTIVE_CELL_DATA_IY_INDEX];
	}

	private void setActiveCellDataIy(int y) {
		activeCellData[ACTIVE_CELL_DATA_IY_INDEX] = y;
	}

	private void addActiveCellDataIy(int y) {
		setActiveCellDataIy(getActiveCellDataIy() + y);
	}

	private int getActiveCellDataY() {
		return activeCellData[ACTIVE_CELL_DATA_Y_INDEX];
	}

	private void setActiveCellDataY(int y) {
		activeCellData[ACTIVE_CELL_DATA_Y_INDEX] = y;
	}

	private byte getNumberOfHoles() {
		return Player.NUMBER_OF_HOLES;
	}

	/**
	 * Width of canvas used for holes and sum cells
	 * 
	 * @return
	 */
	private int getHolesAndSumCellsCanvasWidth() {
		return getWidth() - getNameCellWidth();
	}

	private int getHoleCellWidth() {
		return Settings.getHOLE_CELL_WIDTH();
	}

	private int getNameCellWidth() {
		return Settings.getNAME_CELL_WIDTH();
	}

	private int getCellHeight() {
		return Settings.getCELL_HEIGHT();
	}

	/**
	 * Tells us how many input cells we can render on the page
	 * 
	 * @return
	 */
	private int getMaxInputCellsToRender() {
		boolean lastPage = isLastPage();
		if (lastPage) {
			// on the last page
			int remainingCells = Player.NUMBER_OF_HOLES % getRenderedInputCellsDataPageSize();
			if (remainingCells > 0) {
				return remainingCells;
			}
		}
		return getRenderedInputCellsDataPageSize();
	}

	/**
	 * Last index (starting from 0) on the current page of holes cell
	 * 
	 * @return
	 */
	private int getCurrentPageLastHoleIndex() {
		return isLastPage() ? Player.NUMBER_OF_HOLES - 1 : (getRenderedInputCellsDataPageIndex() + 1) * getRenderedInputCellsDataPageSize() - 1;
	}

	/**
	 * First index (starting from 0) on the current page of holes cell
	 * 
	 * @return
	 */
	private int getCurrentPageFirstHoleIndex() {
		return getRenderedInputCellsDataPageIndex() * getRenderedInputCellsDataPageSize();
	}

	private int getRenderedInputCellsDataPageIndex() {
		return renderedInputCellsData[INPUT_CELLS_RENDERED_PAGE_INDEX];
	}

	private void setRenderedInputCellsDataPageIndex(int index) {
		renderedInputCellsData[INPUT_CELLS_RENDERED_PAGE_INDEX] = index;
	}

	private void addRenderedInputCellsDataPageIndex(int valueToAdd) {
		setRenderedInputCellsDataPageIndex(getRenderedInputCellsDataPageIndex() + valueToAdd);
	}

	private void setRenderedInputCellsDataPageSize(int size) {
		renderedInputCellsData[INPUT_CELLS_RENDERED_PAGE_SIZE] = size;
	}

	private int getRenderedInputCellsDataPageSize() {
		return renderedInputCellsData[INPUT_CELLS_RENDERED_PAGE_SIZE];
	}

	private boolean isLastPage() {
		return (getRenderedInputCellsDataPageIndex() + 1) * getRenderedInputCellsDataPageSize() >= Player.NUMBER_OF_HOLES;
	}

	private int getSumCellWidth(int lastVerticalLineXCoordinate) {
		return getWidth() - getNameCellWidth() - lastVerticalLineXCoordinate;
	}

	private int getStringWidth(String value) {
		return font.stringWidth(value);
	}

	private void setLastKeyCodeNotSet() {
		lastKeyCode = Integer.MAX_VALUE;
	}

	private boolean isLastKeyCodeNotSet() {
		return lastKeyCode == Integer.MAX_VALUE;
	}

	private void setBackgroundColor(Graphics g) {
		g.setColor(0, 100, 0);
	}

	private void setForegroundColor(Graphics g) {
		g.setColor(0xFFFFFF);
	}

	private void setLabelColor(Graphics g) {
		g.setColor(255, 127, 0);
	}

	private void setSumColor(Graphics g) {
		g.setColor(255, 69, 0);
	}

	public static String[] getAvailableMainModesLabelsBundleKeys() {
		return new String[] { Settings.TEXT_MANUAL_MOVE_WITH_INCREMENT_MODE, Settings.TEXT_AUTO_MOVE_MODE };
	}

	/**
	 * If move to next player is requested. Called in some
	 * {@link Mode#handleKeyEvent()}
	 * 
	 * @return
	 */
	private boolean moveDownOccures() {
		return getActiveCellDataIy() < midlet.getPlayers().size() - 1;
	}

	/**
	 * Conforms to this{@link #getAvailableMainModesBundleKeys()} indexes
	 * 
	 * @param index
	 */
	private void setMainMode(int index) {
		if (index == 0) {
			mode = manMoveWithIncrementMode;
		} else {
			mode = autoMoveMode;
		}
	}

	private class AsyncCommand implements Runnable {

		private Command command;

		public void run() {
			if (command != null) {
				try {
					command.execute();
				} catch (Exception e) {
					// TODO show an alert
					e.printStackTrace();
				}
			}
		}

		public void setCommand(Command command) {
			this.command = command;
		}

	}

	private class LastPage implements Command {

		private boolean calledSerially = false;

		public void execute() throws Exception {
			// sort the players
			midlet.sortPlayers();
			firstRenderedLineIndex = 0;
			setLastKeyCodeNotSet();
			setActiveCellXStuffToTopLeft();
			// end of the game mode
			swapMode(endOfTheGameMode);
			if (calledSerially) {
				try {
					Thread.sleep(PAUSE);
				} catch (InterruptedException e) {
				}
				repaint();
			}
		}
	}

	private class HorizontalPaging implements Command {

		private boolean calledSerially = false;

		public void execute() throws Exception {
			handleHorizontalMove(0, addToActiveCellDataIx(1));
			setActiveCellDataY(0);
			setActiveCellDataIy(0);
			addRenderedInputCellsDataPageIndex(1);
			firstRenderedLineIndex = 0;
			if (calledSerially) {
				try {
					Thread.sleep(PAUSE);
				} catch (InterruptedException e) {
				}
				repaint();
			}
		}

		private boolean horizontalPagingOccures() {
			return getActiveCellDataIy() == midlet.getPlayers().size() - 1 && !isLastPage();
		}
	}

	private class VerticalPaging implements Command {

		private void handleVerticalPaging() {
			firstRenderedLineIndex = getActiveCellDataIy();
			setActiveCellDataY(0);
		}

		private int x;

		public void execute() throws Exception {
			handleVerticalPaging();
			setActiveCellDataX(x);
			try {
				Thread.sleep(PAUSE);
			} catch (InterruptedException e) {
			}
			repaint();
		}

		private boolean verticalPagingOccures() {
			return getActiveCellDataY() + 2 * getCellHeight() > getHeight();
		}
	}

	private class ToNextColumnMove implements Command {

		private int x;
		private boolean calledSerially = false;

		public void execute() throws Exception {
			// move to the right and y = 0 if paging does not occur
			setActiveCellDataX(x);
			handleHorizontalMove(getActiveCellDataX() + getHoleCellWidth(), addToActiveCellDataIx(1));
			setActiveCellDataY(0);
			setActiveCellDataIy(0);
			if (calledSerially) {
				if (firstRenderedLineIndex != 0) {
					try {
						Thread.sleep(PAUSE);
					} catch (InterruptedException e) {
					}
				}
				// repaint (not synchronous) can be call here even the
				// firstRenderedLineIndex is changed lately...
				repaint();
			}
			firstRenderedLineIndex = 0;
		}

		/**
		 * move to the next column if hor. paging does not occur
		 * 
		 * @return
		 */
		private boolean toNextColumnMoveOccures() {
			return getActiveCellDataIx() != getCurrentPageLastHoleIndex();
		}
	}

	private interface Mode {
		public void handleKeyEvent();

		public void renderActiveCell(Graphics g, int x, int y);

		public void keyRepeated(int keyCode);

		public void keyPressed(int keyCode);

		public void setCommands();

		public void clearCommands();

		public void renderValueOnActiveCell(Graphics g, String value, int x, int y);

		public AbstractCommand getCommand();

		/**
		 * Called before this is replaced by other mode
		 */
		public void beforeDeactivate();
	}

	private class AutoMoveMode implements Mode {

		private boolean renderActiveCell = true;

		public void handleKeyEvent() {
			byte value = convertLastKeyCode();
			if (value != Byte.MIN_VALUE) {
				// propagate value to the model
				updateModel(value);
				// handle active cell move...
				// we can move down if the x < (users.size - 1)*cell height, else we
				// move to the next hole cell

				if (moveDownOccures()) {
					// just moving down
					handleMoveDown();
				} else if (toNextColumnMove.toNextColumnMoveOccures()) {
					// move to the next column if hor. paging does not occur
					toNextColumnMove.x = getActiveCellDataX();
					toNextColumnMove.calledSerially = true;
					setActiveCellDataX(Integer.MIN_VALUE);
					setLastKeyCodeNotSet();
					asyncCommand.setCommand(toNextColumnMove);
					midlet.callSerially(asyncCommand);
				} else if (horizontalPaging.horizontalPagingOccures()) {
					// horizontal paging
					// just do not render the active cell
					setActiveCellDataX(Integer.MIN_VALUE);
					// ..continue rendering and call paging logic serially
					horizontalPaging.calledSerially = true;
					asyncCommand.setCommand(horizontalPaging);
					setLastKeyCodeNotSet();
					midlet.callSerially(asyncCommand);
				} else if (isLastPage()) {
					// last page, move to the end of the game mode
					setActiveCellDataY(0);
					setActiveCellDataIy(0);
					renderActiveCell = false;
					lastPage.calledSerially = true;
					asyncCommand.setCommand(lastPage);
					midlet.callSerially(asyncCommand);
				}
			}
			// handle the vertical paging
			if (verticalPaging.verticalPagingOccures()) {
				verticalPaging.x = getActiveCellDataX();
				setActiveCellDataX(Integer.MIN_VALUE);
				asyncCommand.setCommand(verticalPaging);
				midlet.callSerially(asyncCommand);
			}

		}

		public void renderActiveCell(Graphics g, int x, int y) {
			if (renderActiveCell) {
				ScoreCardCanvas.this.renderActiveCell(g, x, y);
			}
		}

		public void keyRepeated(int keyCode) {
			// none
		}

		public void keyPressed(int keyCode) {
			customKeyPressed(keyCode);
		}

		public void setCommands() {
			// must add command to the canvas through which you can turn on the manual
			// move mode
			addCommand(manMoveMode.getCommand());
		}

		public void clearCommands() {
			removeCommand(manMoveMode.getCommand());
		}

		public void renderValueOnActiveCell(Graphics g, String value, int x, int y) {
			writeValueWithMarginToHoleCell(value, g, x, y);
		}

		public AbstractCommand getCommand() {
			return getAutoMoveModeCmd();
		}

		public void beforeDeactivate() {
			// none
		}
	}

	private class ManualMoveMode implements Mode {

		public ManualMoveMode(Mode mainMode) {
			super();
			this.mainMode = mainMode;
		}

		private final Mode mainMode;

		public void handleKeyEvent() {
			byte value = convertLastKeyCode();
			if (value != Byte.MIN_VALUE) {
				updateModel(value);
			} else {
				// manual move mode
				// all the moves with vertical and horizontal paging are available
				/**
				 * active cell is standard white, number is black!//
				 */
				if (!isLastKeyCodeNotSet()) {
					// terrible checking!
					int gameAction = ScoreCardCanvas.this.getGameAction(lastKeyCode);
					if (gameAction == Canvas.DOWN) {
						if (moveDownOccures()) {
							// just moving down
							handleMoveDown();
						}
						// handle the vertical paging
						if (verticalPaging.verticalPagingOccures()) {
							verticalPaging.handleVerticalPaging();
						}
					} else if (gameAction == Canvas.LEFT) {
						if (getActiveCellDataIx() > 0) {
							if (getActiveCellDataIx() == getCurrentPageFirstHoleIndex()) {
								// horizontal paging
								handleHorizontalMove((getRenderedInputCellsDataPageSize() - 1) * getHoleCellWidth(), addToActiveCellDataIx(-1));
								addRenderedInputCellsDataPageIndex(-1);
							} else {
								handleHorizontalMove(getActiveCellDataX() - getHoleCellWidth(), addToActiveCellDataIx(-1));
							}
						}
					} else if (gameAction == Canvas.RIGHT) {
						if (toNextColumnMove.toNextColumnMoveOccures()) {
							// no horizontal paging
							handleHorizontalMove(getActiveCellDataX() + getHoleCellWidth(), addToActiveCellDataIx(1));
						} else if (!isLastPage()) {
							// horizontal paging
							handleHorizontalMove(0, addToActiveCellDataIx(1));
							addRenderedInputCellsDataPageIndex(1);
						}
					} else if (gameAction == Canvas.UP) {
						if (getActiveCellDataIy() > 0) {
							if (firstRenderedLineIndex == getActiveCellDataIy()) {
								int substractedIndexes = computeAndSetFirstRenderedLineIndex();
								int activeCellDataY = (substractedIndexes - 1) * getCellHeight();
								setActiveCellDataY(activeCellDataY);
							} else {
								setActiveCellDataY(getActiveCellDataY() - getCellHeight());
							}
							addActiveCellDataIy(-1);
						}
					}
				}
			}
			setLastKeyCodeNotSet();
		}

		public void renderActiveCell(Graphics g, int x, int y) {
			ScoreCardCanvas.this.renderActiveCell(g, x, y);
		}

		public void keyRepeated(int keyCode) {
			customKeyRepated(keyCode);
		}

		public void keyPressed(int keyCode) {
			customKeyPressed(keyCode);
		}

		public void setCommands() {
			if (!endOfTheGameMode.activated) {
				addCommand(mainMode.getCommand());
			} else {
				addCommand(endOfTheGameMode.getCommand());
			}
		}

		public void clearCommands() {
			if (!endOfTheGameMode.activated) {
				removeCommand(mainMode.getCommand());
			} else {
				removeCommand(endOfTheGameMode.getCommand());
			}
		}

		public void renderValueOnActiveCell(Graphics g, String value, int x, int y) {
			// Current cell is active, value is black
			setBackgroundColor(g);
			writeValueWithMarginToHoleCell(value, g, x, y);
			setForegroundColor(g);
		}

		public AbstractCommand getCommand() {
			return getManualMoveModeCmd();
		}

		public void beforeDeactivate() {

		}

	}

	private class ManualMoveWithIncrementMode implements Mode {
		public void handleKeyEvent() {
			byte value = convertLastKeyCode();
			if (value == 1) {
				updateModel((byte) 1, true);
			} else {
				if (!isLastKeyCodeNotSet()) {
					// terrible checking!
					int gameAction = ScoreCardCanvas.this.getGameAction(lastKeyCode);
					if (gameAction == Canvas.DOWN && getCurrentPlayer().isHoleSet(getActiveCellDataIx())) {
						if (moveDownOccures()) {
							// just moving down
							handleMoveDown();
						} else if (toNextColumnMove.toNextColumnMoveOccures()) {
							// move to the next column if hor. paging does not occur
							ToNextColumnMove toNextColumnMove = ScoreCardCanvas.this.toNextColumnMove;
							toNextColumnMove.x = getActiveCellDataX();
							toNextColumnMove.calledSerially = false;
							asyncCommand.setCommand(toNextColumnMove);
							asyncCommand.run();
						} else if (horizontalPaging.horizontalPagingOccures()) {
							// horizontal paging
							horizontalPaging.calledSerially = false;
							asyncCommand.setCommand(horizontalPaging);
							asyncCommand.run();
						} else if (isLastPage()) {
							// last page, move to the end of the game mode
							setActiveCellDataY(0);
							setActiveCellDataIy(0);
							lastPage.calledSerially = false;
							asyncCommand.setCommand(lastPage);
							asyncCommand.run();
						}
					}
					// handle the vertical paging
					if (verticalPaging.verticalPagingOccures()) {
						verticalPaging.handleVerticalPaging();
					}
				}
			}
			setLastKeyCodeNotSet();
		}

		public void renderActiveCell(Graphics g, int x, int y) {
			ScoreCardCanvas.this.renderActiveCell(g, x, y);
		}

		public void keyRepeated(int keyCode) {
			customKeyRepated(keyCode);
		}

		public void keyPressed(int keyCode) {
			customKeyPressed(keyCode);
		}

		public void setCommands() {
			addCommand(manMoveMode.getCommand());
		}

		public void clearCommands() {
			removeCommand(manMoveMode.getCommand());
		}

		public void renderValueOnActiveCell(Graphics g, String value, int x, int y) {
			// Current cell is active, value is black
			setBackgroundColor(g);
			writeValueWithMarginToHoleCell(value, g, x, y);
			setForegroundColor(g);
		}

		public AbstractCommand getCommand() {
			return getManualMoveWithIncrementModeCmd();
		}

		public void beforeDeactivate() {
		}
	}

	private class EndOfTheGameMode implements Mode {

		private boolean activated = false;

		public void handleKeyEvent() {
			if (!isLastKeyCodeNotSet()) {
				/**
				 * active cell is standard white, number is black!//
				 */
				int gameAction = ScoreCardCanvas.this.getGameAction(lastKeyCode);
				if (gameAction == Canvas.DOWN) {
					// handle the vertical paging. we must compute the
					// firstRenderedLineIndex.
					for (int i = firstRenderedLineIndex, y = getCellHeight(); i < midlet.getPlayers().size(); i++, y += getCellHeight()) {
						if (y + getCellHeight() > getHeight()) {
							firstRenderedLineIndex = i;
							break;
						}
					}
				} else if (gameAction == Canvas.LEFT) {
					// move left
					int pageIndex = getRenderedInputCellsDataPageIndex();
					if (getRenderedInputCellsDataPageIndex() > 0) {
						setRenderedInputCellsDataPageIndex(--pageIndex);
					}
				} else if (gameAction == Canvas.UP) {
					// handle the vertical paging
					computeAndSetFirstRenderedLineIndex();
				} else if (gameAction == Canvas.RIGHT) {
					// move right
					if (!isLastPage()) {
						setRenderedInputCellsDataPageIndex(getRenderedInputCellsDataPageIndex() + 1);
					}
				}
				setLastKeyCodeNotSet();
			}
		}

		public void renderActiveCell(Graphics g, int x, int y) {
			// none
		}

		public void keyRepeated(int keyCode) {
			customKeyRepated(keyCode);
		}

		public void keyPressed(int keyCode) {
			customKeyPressed(keyCode);
		}

		public void clearCommands() {
			removeCommand(manMoveMode.getCommand());
		}

		public void renderValueOnActiveCell(Graphics g, String value, int x, int y) {
			writeValueWithMarginToHoleCell(value, g, x, y);
		}

		public void setCommands() {
			addCommand(manMoveMode.getCommand());
		}

		public AbstractCommand getCommand() {
			return getEndOfGameModeCmd();
		}

		public void beforeDeactivate() {
			// Iy must corespond to firstRenderedLineIndex when moving from this
			// mode...
			setActiveCellDataIy(firstRenderedLineIndex);
			setActiveCellDataY(0);
			// when leaving, x stuff must be on the top left...
			setActiveCellXStuffToTopLeft();
			activated = true;
		}
	}
}
